package com.yue92.datalife.service;

import com.yue92.datalife.common.JsonUtil;
import com.yue92.datalife.model.BaseModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
public abstract class AbstractService<T extends BaseModel<ID>, ID> implements IService<T, ID> {

    /**
     * seconds
     */
    private static final Long DEFAULT_TTL = 7 * 24 * 60 * 60L;

    private static final int BATCH_SIZE = 100;

    protected static final String NULL_VALUE = "$N";

    @Autowired
    protected StringRedisTemplate redisTemplate;

    protected abstract void deleteFromDbById(ID id);

    protected abstract void deleteFromDb(List<T> entity);

    protected abstract Iterable<T> saveAllToDb(List<T> list);

    protected abstract Iterable<T> findAllFromDbById(List<ID> ids);

    protected abstract Optional<T> findFromDbById(ID id);

    protected long getCacheTTL() {
        return DEFAULT_TTL;
    }

    protected abstract Class<T> getModelClass();

    @Override
    public Optional<T> get(ID id) {
        String json = getFromCache(id, getModelClass());

        if (NULL_VALUE.equals(json)) {
            return Optional.empty();
        }

        if (json != null) {
            return JsonUtil.toObject(json, getModelClass());
        }

        Optional<T> t = findFromDbById(id);
        if (t.isPresent()) {
            setToCache(t.get());
        } else {
            String key = getCacheKeyFromId(id, getModelClass());
            setToCache(key, NULL_VALUE);
        }
        return t;
    }

    @Override
    public Optional<T> getFromDb(ID id) {
        return findFromDbById(id);
    }

    @Override
    public Map<ID, T> getMap(List<ID> ids) {
        List<T> ret = get(ids);
        Map<ID, T> map = new LinkedHashMap<>();
        ret.forEach(e -> {
            map.put(e.getId(), e);
        });
        return map;
    }

    @Override
    public Map<ID, T> getMapFromDb(List<ID> ids) {
        List<T> ret = getFromDb(ids);
        Map<ID, T> map = new LinkedHashMap<>();
        ret.forEach(e -> {
            map.put(e.getId(), e);
        });
        return map;
    }

    @Override
    public List<T> get(List<ID> ids) {
        if (ids.isEmpty()) {
            return Collections.EMPTY_LIST;
        }

        ids = ids.stream().distinct().collect(Collectors.toList());

        int total = ids.size();

        if (total <= BATCH_SIZE) {
            return batchGets(ids);
        }

        int offset = 0;
        List<T> ret = new ArrayList<>();
        do {
            List<ID> tmps = ids.stream().skip(offset).limit(BATCH_SIZE).collect(Collectors.toList());
            ret.addAll(batchGets(tmps));
            offset += BATCH_SIZE;
        } while (offset < total);

        return ret;
    }

    @Override
    public List<T> getFromDb(List<ID> ids) {
        return (List<T>) findAllFromDbById(ids);
    }

    private List<T> batchGets(List<ID> ids) {
        List<String> jsons = getFromCache(ids, getModelClass());

        Map<ID, T> entityMap = new HashMap<>();
        jsons.forEach(v -> {
            if (!NULL_VALUE.equals(v)) {
                Optional<T> val = JsonUtil.toObject(v, getModelClass());
                val.ifPresent(t -> entityMap.put(t.getId(), t));
            }
        });

        List<ID> uncached = new ArrayList<>();

        ids.forEach(e -> {
            if (!entityMap.containsKey(e)) {
                uncached.add(e);
            }
        });

        if (uncached.size() > 0) {
            Iterable<T> dbEntityList = findAllFromDbById(uncached);
            List<T> list = iterableToList(dbEntityList);
            setToCache(list);
            list.forEach(e -> {
                entityMap.put(e.getId(), e);
            });
            list.forEach(e -> {
                uncached.remove(e.getId());
            });
            if (!uncached.isEmpty()) {
                Map<String, String> saves = new HashMap<>();
                uncached.forEach(e -> {
                    saves.put(getCacheKeyFromId(e, getModelClass()), NULL_VALUE);
                });
                setToCache(saves);
            }
        }

        List<T> result = new ArrayList<>();

        ids.forEach(e -> {
            if (entityMap.containsKey(e)) {
                result.add(entityMap.get(e));
            }
        });

        return result;
    }

    @Override
    public T save(T t) {
        t = saveToDb(t);
        setToCache(t);
        return t;
    }

    @Override
    public List<T> save(List<T> t) {
        if (t.isEmpty()) {
            return t;
        }
        int total = t.size();
        if (total < BATCH_SIZE) {
            return batchSave(t);
        }

        int offset = 0;
        List<T> ret = new ArrayList<>();
        do {
            List<T> tmps = t.stream().skip(offset).limit(BATCH_SIZE).collect(Collectors.toList());
            ret.addAll(batchSave(tmps));
            offset += BATCH_SIZE;
        } while (offset < total);

        return ret;
    }

    @Override
    public List<T> saveToDb(List<T> t) {
        return (List<T>) saveAllToDb(t);
    }

    private List<T> batchSave(List<T> t) {
        Iterable<T> ret = saveAllToDb(t);
        List<T> lst = iterableToList(ret);
        setToCache(t);
        return lst;
    }

    @Override
    public void remove(ID id) {
        deleteFromCache(getCacheKeyFromId(id, getModelClass()));
        deleteFromDbById(id);
    }

    @Override
    public void removeFromDb(ID id) {
        deleteFromDbById(id);
    }

    @Override
    public void remove(List<T> t) {
        t.removeIf(Objects::isNull);
        if (t.isEmpty()) {
            return;
        }
        int total = t.size();
        if (total < BATCH_SIZE) {
            batchRemove(t);
            return;
        }

        int offset = 0;
        do {
            List<T> tmps = t.stream().skip(offset).limit(BATCH_SIZE).collect(Collectors.toList());
            batchRemove(tmps);
            offset += BATCH_SIZE;
        } while (offset < total);
    }

    @Override
    public void removeFromDb(List<T> t) {
        deleteFromDb(t);
    }

    private void batchRemove(List<T> t) {
        List<String> cacheIds = t.stream()
                .map(e -> getCacheKeyFromId(e.getId(), getModelClass()))
                .collect(Collectors.toList());
        deleteFromCache(cacheIds);
        deleteFromDb(t);
    }

    private void setToCache(String key, String value) {

        redisTemplate.opsForValue().set(key, value, getCacheTTL(), TimeUnit.SECONDS);
    }

    private void setToCache(T t) {
        String key = getCacheKeyFromId(t.getId(), getModelClass());

        setToCache(key, JsonUtil.toJson(t).orElse(NULL_VALUE));
    }

    private void setToCache(List<T> list) {
        list.forEach(this::setToCache);
    }

    private void setToCache(Map<String, String> keyValues) {
        keyValues.forEach((k, v) -> redisTemplate.opsForValue().set(k, v, getCacheTTL(), TimeUnit.SECONDS));
    }

    private Boolean deleteFromCache(String key) {
        return redisTemplate.delete(key);
    }

    private Boolean deleteFromCache(List<String> keys) {
        keys.forEach(this::deleteFromCache);
        return true;
    }

    private String getFromCache(ID id, Class<T> clz) {
        String cacheKey = getCacheKeyFromId(id, clz);
        return redisTemplate.opsForValue().get(cacheKey);
    }

    private List<String> getFromCache(List<ID> ids, Class<T> clz) {
        List<String> ret = new ArrayList<>();

        for (ID id : ids) {
            String key = getCacheKeyFromId(id, clz);
            String value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                ret.add(value);
            }
        }

        return ret;
    }

    private String getCacheKey(String key, Class<T> clz) {
        return clz.getSimpleName() + ":" + key;
    }

    private String getCacheKeyFromId(ID id, Class<T> clz) {
        return getCacheKey("id:" + id.toString(), clz);
    }

    private <S> List<S> iterableToList(Iterable<S> iterable) {
        List<S> list = new ArrayList<>();
        for (S i : iterable) {
            list.add(i);
        }
        return list;
    }
}
